/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daios.wsdl.matcher;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.logging.Logger;

import at.ac.tuwien.infosys.dsg.daios.exceptions.AmbigiousException;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosInputMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDL;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLOperation;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLPort;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLService;
import at.ac.tuwien.infosys.dsg.daios.wsdl.typesystem.WSDLTypeTree;

/**
 * This is the abstract superclass of all invocation
 * matchers.
 * 
 * <p>
 * 
 * The responsibility of
 * at.ac.tuwien.infosys.dsg.daios.wsdl.matcher.Matcher
 * and its subclasses is to compare Daios messages to
 * WSDL elements, and decide on the  structure of
 * the WSDL components and the Daios message. Since
 * this matching is different for each invocation style
 * there is a concrete subclass of
 * at.ac.tuwien.infosys.dsg.daios.wsdl.matcher.Matcher
 * for each supported Web Service invocation style.
 * 
 * <p>
 * 
 * Use
 * <link>at.ac.tuwien.infosys.dsg.daios.wsdl.matcher.WSDLStyleMapper</link>
 * as a factory to access a new matcher instance for your
 * invocation style.
 * 
 * @author philipp
 *
 */
abstract public class Matcher {
	
	/**
	 * This constant defines "infinite distance".
	 */
	public static final int MAX_DISTANCE =
		Integer.MAX_VALUE;
	
	// the logger
	protected Logger log = Logger.getLogger(IConstants.LOG_NAME);
	
	/**
	 * Guess an endpoint address from the definitions.
	 * 
	 * <p>
	 * 
	 * This works only if there is exactly one service
	 * with exactly one port, which has a SOAP address
	 * extensibility element. This is not such a hard
	 * constraint since most services are set up that way.
	 * 
	 * <p>
	 * 
	 * If this method throws an AmbigiousException the
	 * endpoint has to be set explicitly prior to invoking.
	 * 
	 * @param wsdl The WSDL definition.
	 * @return The endpoint address as String.
	 * 
	 * @throws AmbigiousException
	 */
	public String getEndpoint(IWSDL wsdl) throws AmbigiousException {
		
		// check if there is more than one service / endpoint in this
		// definition
		List<IWSDLService> services = null;
		if((services = wsdl.getServices()).size() > 1)
			throw new AmbigiousException("There are "+wsdl.getServices().size()+
						" services at this location.");
		
		IWSDLService service = (IWSDLService)
			services.get(0);
		
		// check if there is more than one port in this
		// service
		List<IWSDLPort> ports = null;
		if((ports = service.getPorts()).size() > 1)
			throw new AmbigiousException("There are "+ports.size()+
						" ports to this service.");
		
		IWSDLPort port = (IWSDLPort)
			ports.get(0);
		
		String endpoint = port.getURILocation();
		
		return endpoint;
	}
	
	/**
	 * Get a WSDL operation that has a given WSDL message
	 * as input. Operations that take this message as
	 * output or fault message will not be considered.
	 * 
	 * <p>
	 * 
	 * This method fails with an AmbigiousException if there
	 * is more than one operation in the WSDL definition that
	 * takes this message as input.
	 * 
	 * @param msg The message.
	 * @return The operation that takes msg as input.
	 * 
	 * @throws AmbigiousException
	 */
	public IWSDLOperation getOperation(IWSDL wsdl, IWSDLMessage msg)
		throws AmbigiousException {
		
		String msgName = msg.getMessageName();
		List<IWSDLOperation> ops = wsdl.getOperations();
		
		boolean found = false;
		IWSDLOperation foundOp = null;
		for(IWSDLOperation op:ops) {
			if(op.getInput().equals(msg.getMessageName())) {
				if(found)
					throw new AmbigiousException("Input Message "+msgName+
						" is ambigious.");
				foundOp = op;
				found = true;
			}
		}
		
		return foundOp;
	}

	/**
	 * Get the WSDL message that is most similar to a given
	 * Daios input message. This is the main functionality
	 * provided by any matcher. The general algorithm is that
	 * the <i>distance</i> between any message in the WSDL file
	 * and the Daios input message is calculated, and the message
	 * with the lowest distance to the input is returned.
	 * 
	 * <p>
	 * 
	 * This algorithm is used in any concrete matcher, but the
	 * algorithm to calculate the distance between input and
	 * WSDL messages has to be specifically overwritten in all 
	 * subclasses.
	 * 
	 * @param wsdl The WSDL definition to use.
	 * @param input The Daios input message.
	 * @return The WSDL message that has the lowest distance to
	 * the given input message.
	 * @throws AmbigiousException Thrown to indicate that 2 or
	 * more messages in the definition are bound for the lowest
	 * distance to the input.
	 */
	public IWSDLMessage getSimilarMsg(IWSDL wsdl, DaiosInputMessage input)
		throws AmbigiousException {
		
		// all messages
		List<IWSDLMessage> messages = wsdl.getMessages();
		
		// all types
		WSDLTypeTree types = wsdl.getTypes();
		
		
		// calculate distances
		Hashtable<IWSDLMessage,Integer> distances =
			calculateDistanceMatrix(wsdl,input, messages, types);
		
		log.info("Distance Table:\n"+distances.toString());
		
		// now return the message with the lowest distance
		// throw an error if two messages are bound for the lowest
		// distance
		int min = MAX_DISTANCE;
		int thisDistance = -1;
		IWSDLMessage best = null;
		Enumeration<IWSDLMessage> msgs = distances.keys();
		IWSDLMessage m = null;
		while(msgs.hasMoreElements()) {
			m = msgs.nextElement();
			thisDistance = distances.get(m);
			if(thisDistance < min) {
				min = thisDistance;
				best = m;
			}
		}
		msgs = distances.keys();
		m = null;
		int counter = 0;
		while(msgs.hasMoreElements()) {
			m = msgs.nextElement();
			thisDistance = distances.get(m);
			if(thisDistance == min) {
				counter++;
			}
		}

		if(min == MAX_DISTANCE)
			throw new AmbigiousException("No message in this definition "+
				"is similar to the one provided by you.");
		
		if(counter > 1)
				throw new AmbigiousException("There are "+counter+" "+
					"messages in this definition which are equally similar "+
					"to your input message.");
		
		// fine, no problem occured - return the best match
		return best;
	
	}

	/**
	 * Check if a given WSDL message is a possible input message for our
	 * definition, and if the given Daios message can be converted into
	 * such a WSDL message.
	 * 
	 * @param wsdl The WSDL definitions.
	 * @param msgToUse The WSDL message to check.
	 * @param message The Daios message to check against.
	 * 
	 * @throws AmbigiousException If the WSDL message is not compliant.
	 */
	public void checkCompliance(IWSDL wsdl, IWSDLMessage msgToUse, DaiosInputMessage message)
		throws AmbigiousException {
		
		// check if this message is an input message
		if(!isInputMessage(msgToUse, wsdl.getOperations()))
			throw new AmbigiousException("WSDL Message "+msgToUse.getMessageName()+
				" is no input message.");
		
		// check if the structure of the Daios and the WSDL message are
		// compatible
		if(distance(message,msgToUse,wsdl.getOperations(),wsdl.getTypes()) == MAX_DISTANCE)
			throw new AmbigiousException("WSDL Message "+msgToUse+" cannot be "+
				"converted into your Daios input.");
		
	}
	
	/**
	 * Checks if a given WSDL message is used as an input
	 * message in at least one operation in this WSDL 
	 * definition.
	 * 
	 * @param m The WSDL message.
	 * @param ops All WSDL operations defined
	 * @return True if an input message, false otherwise.
	 */
	protected boolean isInputMessage(IWSDLMessage m, List<IWSDLOperation> ops) {
		
		
		for(IWSDLOperation op : ops)
			if(op.getInput() !=null && op.getInput().equals(m.getMessageName()))
				return true;
		
		return false;
	}
	
	/**
	 * Calculate a distance matrix. The distance matrix maps all WSDL
	 * messages to their distance to a given input message.
	 * 
	 * @param wsdl The WSDL definitions to use.
	 * @param input The Daios input message to compare to
	 * calculate the distance to.
	 * @param messages A list of all messages to compare.
	 * @param types A type tree containing all types from the
	 * WSDL definitions.
	 * @return The distance matrix as hashtable.
	 * 
	 * @throws AmbigiousException
	 */
	protected Hashtable<IWSDLMessage, Integer>
		calculateDistanceMatrix(IWSDL wsdl, DaiosInputMessage input,
				List<IWSDLMessage> messages, WSDLTypeTree types)
				throws AmbigiousException {
		
		Hashtable<IWSDLMessage, Integer> mat = 
			new PrintableTable<IWSDLMessage, Integer>();
		
		List<IWSDLOperation> ops = wsdl.getOperations();
		
		// go over each message and calculate the distance
		for(IWSDLMessage m: messages) {
			mat.put(m, distance(input,m,ops,types));
		}
		
		return mat;
	
	}

	/**
	 * Calculate the distance between a single WSDL message and a
	 * Daios input message.
	 * 
	 * <p>
	 * 
	 * This is one of the methods that have to be implemented by all
	 * concrete subclasses of Matcher.
	 * 
	 * @param message The Daios input message.
	 * @param msgToUse The WSDL message to compare to the Daios message.
	 * @param operations A list of all operations to consider.
	 * @param types A type tree containing all types from the WSDL definition.
	 * @return The distance between the Daios message and the WSDL message as Integer.
	 * 
	 * @throws AmbigiousException
	 */
	protected abstract int distance(DaiosInputMessage message, IWSDLMessage msgToUse,
			List<IWSDLOperation> operations, WSDLTypeTree types) throws AmbigiousException;
	
	/**
	 * Private helper class that implements a hashtable
	 * with pretty-print functonality.
	 * 
	 * @author philipp
	 *
	 */
	class PrintableTable<N,M> extends Hashtable<N,M> {
		
		private static final long serialVersionUID = 2978491120624041491L;
		
		/**
		 * Print this hashtable.
		 */
		public String toString() {
			
			StringBuffer sb = new StringBuffer();
			
			Enumeration keys = this.keys();
			Object o = null;
			while(keys.hasMoreElements()) {
				o = keys.nextElement();
				sb.append(o);
				sb.append(" : ");
				sb.append(this.get(o));
				sb.append("\n");
			}
			
			return sb.toString();
		}
		
	}
}
